/*******************************************************************************
blogger-cli Консольные инструменты по работе с blogger.com
(с) Камнев Георгий Павлович 2011 GPLv2

Данная программа является свободным программным обеспечением. Вы вправе
распространять ее и/или модифицировать в соответствии с условиями версии 2
либо по вашему выбору с условиями более поздней версии
Стандартной Общественной Лицензии GNU, опубликованной Free Software Foundation.

Мы распространяем данную программу в надежде на то, что она будет вам полезной,
однако НЕ ПРЕДОСТАВЛЯЕМ НА НЕЕ НИКАКИХ ГАРАНТИЙ,
в том числе ГАРАНТИИ ТОВАРНОГО СОСТОЯНИЯ ПРИ ПРОДАЖЕ
и ПРИГОДНОСТИ ДЛЯ ИСПОЛЬЗОВАНИЯ В КОНКРЕТНЫХ ЦЕЛЯХ.
Для получения более подробной информации ознакомьтесь
со Стандартной Общественной Лицензией GNU.

Вместе с данной программой вы должны были получить экземпляр
Стандартной Общественной Лицензии GNU.
Если вы его не получили, сообщите об этом в Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*******************************************************************************/

package tv.cofe.blogger;

import com.google.gdata.client.Query;
import com.google.gdata.client.blogger.BloggerService;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.Entry;
import com.google.gdata.data.Feed;
import com.google.gdata.data.HtmlTextConstruct;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.gocha.collection.Convertor;
import org.gocha.collection.Iterators;
import org.gocha.files.FileUtil;
//import org.gocha.jdk.JaninoCompiler;
import org.gocha.jdk.LocalMessagesImplementor;
import org.gocha.text.IndentStackWriter;
import org.gocha.text.TextUtil;

/**
 * Точка входа
 * @author gocha
 */
public class CLI
{
    private Map<String,String> execArgs = null;
    
    public CLI(){
        execArgs = new HashMap<String, String>();
    }
    
    public CLI(Map<String,String> execArgs, File appDir){
        if (execArgs== null) {            
            throw new IllegalArgumentException("execArgs==null");
        }
        if (appDir== null) {            
            throw new IllegalArgumentException("appDir==null");
        }
        this.execArgs = execArgs;
        this.appDir = appDir;
    }
    
    // <editor-fold defaultstate="collapsed" desc="Сообщения messages">
    private static Messages messages = null;
    public static Messages messages(){
        return messages;
    }
    
    private static void initMessages(File appd){
        if( messages!=null )return;
        if (appd== null) {            
            throw new IllegalArgumentException("appd==null");
        }
        
        LocalMessagesImplementor lmi = new LocalMessagesImplementor();
        lmi.setCompiler(new org.gocha.jdk.JaninoCompiler());
        lmi.setClassName(Messages.class.getName()+"Impl");
        lmi.setInterface(Messages.class);
        lmi.setClassRootDirectory(new File(new File(appd,"cache"),"classes"));
        lmi.setMessageDirectory(new File(appd,"locale"));
        
        // Устанавливаются по умолчанию
//        lmi.setCacheByteCode(true);
//        lmi.setFileFormat(LocalMessagesImplementor.FileFormat.xml);
//        lmi.setMessageFilePrefix("messages");
        
        Object o = lmi.getInstance();
        if( o instanceof Messages ){
            messages = (Messages)o;
            return;
        }
        throw new Error( "failed init messages" );
    }
    // </editor-fold>
    
    public static String METAFEED_URL = "http://www.blogger.com/feeds/default/blogs";
    public static String FEED_URI_BASE = "http://www.blogger.com/feeds";
    public static String POSTS_FEED_URI_SUFFIX = "/posts/default";
    public static String COMMENTS_FEED_URI_SUFFIX = "/comments/default";
    
    // <editor-fold defaultstate="collapsed" desc="eval() - интерпретация аргументов">
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Do
    {
        String desc() default "";
    }
    
    public static class MenuItem
    {
        protected Do info;
        protected Method method;
        protected Object owner;
        
        public Do getInfo() {
            return info;
        }
        
        public void setInfo(Do info) {
            this.info = info;
        }
        
        public Method getMethod() {
            return method;
        }
        
        public void setMethod(Method method) {
            this.method = method;
        }
        
        public Object getOwner() {
            return owner;
        }
        
        public void setOwner(Object owner) {
            this.owner = owner;
        }
        
        public void eval() {
            if (method == null) return;
            try {
                method.invoke(owner);
            } catch (IllegalAccessException ex) {
                throw new Error(ex.getMessage(), ex);
            } catch (IllegalArgumentException ex) {
                throw new Error(ex.getMessage(), ex);
            } catch (InvocationTargetException ex) {
                throw new Error(ex.getMessage(), ex);
            }
        }
        
        public static MenuItem[] find(Object owner) {
            if (owner == null) {
                throw new IllegalArgumentException("owner==null");
            }
            Class clazz = owner.getClass();
            Method[] methods = clazz.getMethods();
            ArrayList<MenuItem> l = new ArrayList<MenuItem>();
            for (Method m : methods) {
                Do d = m.getAnnotation(Do.class);
                if (d == null)
                    continue;
                if (m.getParameterTypes().length == 0) {
                    MenuItem mi = new MenuItem();
                    mi.setInfo(d);
                    mi.setMethod(m);
                    mi.setOwner(owner);
                    l.add(mi);
                }
            }
            return l.toArray(new MenuItem[]{});
        }
    }
    
    public void eval() {
        MenuItem[] _mi = MenuItem.find(this);
        
        HashMap<String,String> v = new HashMap<String, String>();
        HashMap<String,String> doDesc = new HashMap<String, String>();

        for( MenuItem mi : _mi ){
            String message = null;
            try{
                String desc = mi.getInfo().desc();
                if( desc!=null && desc.length()>0 ){
                    Object res = messages().getClass().getMethod(desc).invoke(messages());
                    if( res!=null && res instanceof String )message = (String)res;
                }
            }
            catch(Throwable ex){
                System.out.println("err "+ex);
            }
            if( message==null || message.length()==0 )message = mi.getInfo().desc();
            doDesc.put(mi.getMethod().getName(), message);
        }
        
		if( !isArgumentExists("do", true) ){
			v.put("do", messages()._do());
			v.put("desc", messages().desc());
			TextUtil.printTemplate(System.out, "{do:>15} {desc:60}", v);
			for( String k : doDesc.keySet() ){
				v.put("do", k );
				v.put("desc", doDesc.get(k));
				TextUtil.printTemplate(System.out, "{do:>15} {desc:60}", v);
			}
		}
        
        String d = getDo();
        if (d == null)return;
        boolean found = false;
        for( MenuItem mi : _mi ){
            Method m = mi.getMethod();
			String methodName = m.getName();
            if( d.equals(methodName) ){
                if( doDesc.containsKey(d) )
                    System.out.println(doDesc.get(d)+":");
                
                mi.eval();
                found = true;
            }
        }
        if( !found ){
            System.out.println(messages().notValidDoArgument());
        }
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Чтение аргументов">
    // <editor-fold defaultstate="collapsed" desc="storage">
    private Properties store = null;
    
    private String readStoredArg(String name, String def) {
        if (store == null)
            store = readStore();
        if (store != null && store.containsKey(name))
            return store.getProperty(name, def);
        return def;
    }

    private void writeStoredArg(String name, String value) {
        if (store == null)
            store = readStore();
        if (store == null)
            store = new Properties();
        store.put(name, value);
        writeStore(store);
    }

    private void removeStoredArg(String name) {
        if (store == null)
            store = readStore();
        if (store == null)
            store = new Properties();
        store.remove(name);
        writeStore(store);
    }

    private void writeStore(Properties p) {
        File args = getStoreFile();
        if (!args.getParentFile().exists()) {
            args.getParentFile().mkdirs();
        }
        try {
            FileUtil.writeProperties(args, p);
        } catch (IOException ex) {
            Logger.getLogger(CLI.class.getName()).log(Level.WARNING, null, ex);
        }
    }

    private Properties readStore() {
        File args = getStoreFile();
        if (args.exists() && args.isFile() && args.canRead()) {
            try {
                Properties _store = FileUtil.readProperties(args);
                return _store;
            } catch (IOException ex) {
                Logger.getLogger(CLI.class.getName()).log(Level.WARNING, null, ex);
            }
        }
        return null;
    }

    private File getStoreFile() {
        return new File(getAppDir(), "args.xml");
    }// </editor-fold>
    
    private Boolean overwriteAllArgs = null;
    
    public boolean isArgumentExists(String name,boolean inStore){
        if (execArgs.containsKey(name)) return true;
        String stored = readStoredArg(name, null);
        return inStore ? stored !=null : false;
    }
    
    public String readArgument(String name, String defValue, boolean isPassword, boolean allowOtherArgs, Boolean _learn) {
        Scanner in = System.console()==null ?
                new Scanner( System.in ) :
                new Scanner( System.console().reader() );

        PrintWriter out = 
                System.console()==null ?
                new PrintWriter(System.out):
                System.console().writer();

        if (execArgs.containsKey(name)) return execArgs.get(name);
        boolean learn = _learn==null ?
                (allowOtherArgs && getArgMode()==ArgumentMode.Learning) :
                (boolean)_learn;
        
        String stored = readStoredArg(name, null);
        if( stored!=null ){
            execArgs.put(name, stored);
            if( isVerbose() )out.println(messages().readedArgumentFromFile(name, getStoreFile()));
            out.flush();
            if( overwriteAllArgs!=null && !overwriteAllArgs ){
                return stored;
            }
            if( learn && ( overwriteAllArgs==null ) ){
                out.println(messages().redefineArgument(name));
                out.print("> ");
                out.flush();
                String overWrite = in.nextLine();
                if( overWrite.equals("n") )return stored;
                if( overWrite.equals("Y") ){
                    overwriteAllArgs = true;
                }
                if( overWrite.equals("N") ){
                    overwriteAllArgs = false;
                    return stored;
                }
            }else{
                return stored;
            }
        }
        
        if (allowOtherArgs && isReadStdInArg()){
			String eol = System.getProperty("line.separator");
			if( eol==null )eol = "\n";

            String line = null;
            out.println(messages().enterArgument(name));
            if( defValue!=null )out.println(messages().defaultArgumentValue(defValue));
			
			while( true ){
				out.print("> ");
				out.flush();

				String l = null;
				if( System.console()!=null ){
					l = isPassword ? new String(System.console().readPassword()) : in.nextLine();
				}else{
					l = in.nextLine();
				}

				if( l.endsWith("\\") && !isPassword ){
					if( line==null )line = TextUtil.trimEnd(l, "\\");
					else line = line + eol + TextUtil.trimEnd(l, "\\");
					continue;
				}else{
					if( line==null )line = l;
					else line = line + eol + l;
				}
				break;
			}

            if( learn && line!=null ){
                out.println(messages().saveArgumentValue());
                out.print( "> " );
                out.flush();
                String save = in.nextLine();
                if( save!=null ){
                    if( save.trim().toLowerCase().equals("y") ){
                        writeStoredArg(name, line);
                    }else if( save.trim().toLowerCase().equals("n") ){
                        removeStoredArg(name);
                    }
                }
            }
            
            execArgs.put(name, line);
            return line;
        }
        return defValue;
    }
    
    public String argument(String name, String defValue) {
        return readArgument(name, defValue, false, true, null);
    }
    
    public String argument(String name, String defValue,boolean allowOtherArgs ) {
        return readArgument(name, defValue, false, allowOtherArgs, null);
    }
    
    public boolean argument(String name, boolean defValue) {
        String a = argument(name, null);
        if( a==null )return defValue;
        
        String val = a;
        if (val == null)return defValue;
        if (val.equalsIgnoreCase("true"))return true;
        if (val.equalsIgnoreCase("yes"))return true;
        if (val.equalsIgnoreCase("on"))return true;
        if (val.equalsIgnoreCase("false"))return false;
        if (val.equalsIgnoreCase("no"))return false;
        if (val.equalsIgnoreCase("off"))return false;
        
        return defValue;
    }
    
    public boolean argument(String name, boolean defValue,boolean allowOtherArgs) {
        String a = readArgument(name, null, false, allowOtherArgs, null);
        if( a==null )return defValue;
        
        String val = a;
        if (val == null)return defValue;
        if (val.equalsIgnoreCase("true"))return true;
        if (val.equalsIgnoreCase("yes"))return true;
        if (val.equalsIgnoreCase("on"))return true;
        if (val.equalsIgnoreCase("false"))return false;
        if (val.equalsIgnoreCase("no"))return false;
        if (val.equalsIgnoreCase("off"))return false;
        
        return defValue;
    }
    
//    public long argumentTime(String name,long defaultTimeStamp,boolean allowOtherArgs){
//        String v = argument(name, Long.toString(defaultTimeStamp), allowOtherArgs);
//    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Аргументы">
    // <editor-fold defaultstate="collapsed" desc="Аргумент: appDir">
    private File appDir = null;

    public File getAppDir() {
        if (appDir != null)
            return appDir;
        String def = System.getProperty("user.home", null);
        if (def != null) {
            String fileSeparator = System.getProperty("file.separator", "/");
            def = TextUtil.trimEnd(def, fileSeparator) + "/.tv/cofe/blogger".replace("/", fileSeparator);
        }else{
            throw new Error("System.getProperty() == null");
        }
        
        if (execArgs.containsKey("appDir")) {
            appDir = new File(execArgs.get("appDir"));
        } else {
            appDir = new File(def);
        }        
        return appDir;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: ArgumentMode">
    public static enum ArgumentMode
    {
        Normal,
        Learning
    }
    private ArgumentMode mode = null;

    public ArgumentMode getArgMode() {
        if (mode != null)
            return mode;
        String m = readArgument("argumentMode", null, false, false, null);
        if (m == null)
            m = readStoredArg("argumentMode", null);
        
        if (m != null && m.equalsIgnoreCase("normal")) {
            mode = ArgumentMode.Normal;
        } else if (m != null && m.toLowerCase().startsWith("learn")) {
            mode = ArgumentMode.Learning;
        } else {
            mode = ArgumentMode.Normal;
        }
        return mode;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: do">
    private String _do = null;

    public String getDo() {
        if (_do != null)
            return _do;
        _do = readArgument("do", null, false, true, false);
        return _do;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: verbose">
    private Boolean verbose = null;

    public Boolean isVerbose() {
        if (verbose != null)
            return verbose;
        String v = readArgument("verbose", "false", false, false, false);
		if( v.equalsIgnoreCase("true") )verbose = true;
		else if( v.equalsIgnoreCase("on") )verbose = true;
		else if( v.equalsIgnoreCase("1") )verbose = true;
		else if( v.equalsIgnoreCase("false") )verbose = false;
		else if( v.equalsIgnoreCase("off") )verbose = false;
		else if( v.equalsIgnoreCase("0") )verbose = false;
		else verbose = false;
        return verbose;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: feedSplitter">
    private String feedSplitter = null;
    public String getFeedSplitter() {
        if (feedSplitter != null)return feedSplitter;
        feedSplitter = argument("feedSplitter", "\n");
        return feedSplitter;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: readStdInArg">
    private Boolean readStdInArg = null;

    /**
     * Аргумент readStdInArg: true - Допускается чтение из SIDIO/Консольни недостающих параметров
     * @return true - Допускается чтение из SIDIO/Консольни
     */
    public Boolean isReadStdInArg() {
        if (readStdInArg != null)
            return readStdInArg;
        if (execArgs.containsKey("readStdInArg")) {
            String val = execArgs.get("readStdInArg");
            if (val.equalsIgnoreCase("true"))
                readStdInArg = true;
            else if (val.equalsIgnoreCase("yes"))
                readStdInArg = true;
            else if (val.equalsIgnoreCase("on"))
                readStdInArg = true;
            else if (val.equalsIgnoreCase("false"))
                readStdInArg = false;
            else if (val.equalsIgnoreCase("no"))
                readStdInArg = false;
            else if (val.equalsIgnoreCase("off"))
                readStdInArg = false;
            else
                readStdInArg = true;
        } else {
            readStdInArg = true;
        }
        return readStdInArg;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: gmailLogin">
    private String gmailLogin = null;

    public String getGmailLogin() {
        if (gmailLogin != null)
            return gmailLogin;
        gmailLogin = argument("gmailLogin", null);
        return gmailLogin;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: gmailPassword">
    private String gmailPassword = null;
    
    public String getGmailPassword() {
        if (gmailPassword != null)
            return gmailPassword;
        gmailPassword = argument("gmailPassword", null);
        return gmailPassword;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: appName">
    private String appName = null;
    
    public String getAppName() {
        if (appName != null)
            return appName;
        appName = argument("appName", "blogger-tools");
        return appName;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: blogID">
    private String blogID = null;

    public String getBlogID() {
        if (blogID != null)
            return blogID;
        blogID = argument("blogID", null);
        return blogID;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: feedTmpl">
    private String feedTmpl = null;

    public String getFeedTmpl() {
        if (feedTmpl != null)
            return feedTmpl;
        feedTmpl = argument("feedTmpl", "{_vars}");
        return feedTmpl;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: exportFileTmpl">
    private String exportFileTmpl = null;

    public String getExportFileTmpl() {
        if (exportFileTmpl != null)
            return exportFileTmpl;
        exportFileTmpl = argument("exportFileTmpl", "./{id}.html");
        return exportFileTmpl;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: exportFile">
    private String exportFile = null;

    public String getExportFile() {
        if (exportFile != null)
            return exportFile;
        exportFile = argument("exportFile", "./exported.html");
        return exportFile;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: limitEntries">
    private Integer limitEntries = null;

    public int getLimitEntries() {
        if (limitEntries != null)
            return limitEntries;
        String v = argument("limitEntries", "25");
        if (v == null)
            throw new Error("limitEntries=null");
        try {
            int r = Integer.parseInt(v);
            if (r < 0)
                throw new Error("limitEntries<0");
            limitEntries = r;
        } catch (NumberFormatException ex) {
            throw new Error(ex.getMessage(), ex);
        }
        return limitEntries;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: postID">
    private String postID = null;

    public String getPostID() {
        if (postID != null)
            return postID;
        postID = argument("postID", null);
        return postID;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: htmlMetaTmpl">
    private String htmlMetaTmpl = null;

    public String getHtmlMetaTmpl() {
        if (htmlMetaTmpl != null)
            return htmlMetaTmpl;
        htmlMetaTmpl = argument("htmlMetaTmpl", "<meta name=\"{key}\" content=\"{value}\" />");
        return htmlMetaTmpl;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: htmlTmpl">
    private String htmlTmpl = null;

    public String getHtmlTmpl() {
        if (htmlTmpl != null)
            return htmlTmpl;
        htmlTmpl = argument("htmlTmpl",
                "<!DOCTYPE html>"
                + "<html>"
                + "<head>"
                + "<title>{title}</title>"
                + "<meta http-equiv=\"content-type\" content=\"text/html; charset={charset}\"/>"
                + "{htmlMeta}"
                + "</head>"
                + "<body>"
                + "{htmlBody}"
                + "</body>"
                + "</html>");
        return htmlTmpl;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: dateFormat">
    private String dateFormatPattern = null;
    public String getDateFormat() {
        if (dateFormatPattern != null)return dateFormatPattern;
        dateFormatPattern = argument("dateFormat","yyyy-MM-dd HH:mm",false);
        return dateFormatPattern;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: addPostFile">
    private String addPostFile = null;
    public String getAddPostFile() {
        if (addPostFile != null)return addPostFile;
        addPostFile = argument("addPostFile",null);
        return addPostFile;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: postTitle">
    private String postTitle = null;
    public String getPostTitle() {
        if (postTitle != null)return postTitle;
        postTitle = argument("postTitle",null,false);
        return postTitle;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: draftPost">
    private Boolean draftPost = null;
    public boolean isDraftPost() {
        if (draftPost != null)return draftPost;
        draftPost = argument("draftPost",false);
        return draftPost;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: timeZone">
    private java.util.TimeZone timeZone = null;
    public java.util.TimeZone getTimeZone() {
        if (timeZone != null)return timeZone;
        String sTimeZone = argument("timeZone","default");
        if( sTimeZone==null ){
            timeZone = java.util.TimeZone.getDefault();
        }else{
            if( sTimeZone.equalsIgnoreCase("default") ){
                timeZone = java.util.TimeZone.getDefault();
            }else{
                timeZone = java.util.TimeZone.getTimeZone(sTimeZone);
            }
        }
        return timeZone;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: timeShift">
    private Long timeShift = null;
    public Long getTimeShift() {
        if (timeShift != null)return timeShift;
        String v = argument("timeShift","00:00");
        if( v!=null ){
            try{
                long ts = TextUtil.stringToTimeStamp(v);
                timeShift = ts;
            }catch(Throwable e){
                timeShift = 0L;
            }
        }
        return timeShift;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: publishedDate">
    private Date publishedDate = null;
    public Date getPublishedDate() {
        if (publishedDate != null)return publishedDate;
        String v = argument("publishedDate",null, false);
        if( v!=null ){
            try{
                if( v.equalsIgnoreCase("now") ){
                    publishedDate = new Date();
                }else{
                    Date d = dateParser().parse(v);
                    publishedDate = d;
                }
            }catch(Throwable e){
                publishedDate = null;
            }
        }
        return publishedDate;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: editedDate">
    private Date editedDate = null;
    public Date getEditedDate() {
        if (editedDate != null)return editedDate;
        String v = argument("editedDate",null, false);
        if( v!=null ){
            try{
                if( v.equalsIgnoreCase("now") ){                    
                    editedDate = new Date();
                }else{
                    Date d = dateParser().parse(v);
                    editedDate = d;
                }
            }catch(Throwable e){
                editedDate = null;
            }
        }
        return editedDate;
    }// </editor-fold>
    // <editor-fold defaultstate="collapsed" desc="Аргумент: updatedDate">
    private Date updatedDate = null;
    public Date getUpdatedDate() {
        if (updatedDate != null)return updatedDate;
        String v = argument("updatedDate",null, false);
        if( v!=null ){
            try{
                if( v.equalsIgnoreCase("now") ){
                    updatedDate = new Date();
                }else{
                    Date d = dateParser().parse(v);
                    updatedDate = d;
                }
            }catch(Throwable e){
                updatedDate = null;
            }
        }
        return updatedDate;
    }// </editor-fold>
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="BloggerService">
    private BloggerService service = null;

    public BloggerService getService() {
        if (service != null)return service;
        String login = getGmailLogin();
        String psswd = getGmailPassword();
        if (login == null)throw new ArgumentNotSetException("gmailLogin");
        if (psswd == null)throw new ArgumentNotSetException("gmailPassword");
        service = new BloggerService(getAppName());
        try {
            service.setUserCredentials(login, psswd);
        } catch (AuthenticationException ex) {
            throw new Error(ex.getMessage(),ex);
        }
        return service;
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="id convertors">
    protected static String getBlogID(Entry e) {
        return e.getId().split("blog-")[1];
    }
    public static final Convertor<Entry, String> blogIDConvertor =
            new Convertor<Entry, String>()
            {
                @Override
                public String convert(Entry from) {
                    return getBlogID(from);
                }
            };
    
    protected static String getPostID(Entry e) {
        return e.getId().split("post-")[1];
    }
    public static final Convertor<Entry, String> postIDConvertor =
            new Convertor<Entry, String>()
            {
                @Override
                public String convert(Entry from) {
                    return getPostID(from);
                }
            };
    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="println feed">
    public void println(Feed feed,Convertor<Entry,String> idConvert) {
		String feedSplttr = getFeedSplitter();
		
        if (feed.getEntries().size() > 0) {
            HashMap<String, String> v = new HashMap<String, String>();
            
            int totCo = feed.getTotalResults();
            boolean totCoUndef = totCo == Query.UNDEFINED;
            int co = 0;
            int maxCo = totCoUndef ? getLimitEntries() : totCo;
            
            for (Entry e : new EntryIterable(feed)) {
                if( e==null )continue;
				if( co>0 )System.out.print(feedSplttr);
					
                v.putAll( PostHelper.getFlatProperties(e,idConvert,dateTimeConvertor) );
                v.put("_vars",
                        TextUtil.join(
                            Iterators.toArray(v.keySet(), new String[]{}), 
                            ",")
                        );
                
                if( co==0 ){
                    HashMap<String,String> vt = new HashMap<String, String>();
                    for(String k:v.keySet())vt.put(k, k);
                    TextUtil.printTemplate(System.out, getFeedTmpl(), vt);
                }
                
                TextUtil.printTemplate(System.out, getFeedTmpl(), v);

                co++;
                if (co >= maxCo)break;
            }
            
            if( isVerbose() )System.out.println(messages().entriesCount(co));
        } else {
            System.out.println(messages().entriesNotExists());
        }
    }// </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="getPostsFeed()">
    /**
     * Получение фида постов
     * @return фид постов
     * @throws ServiceException
     * @throws IOException 
     */
    public Feed getPostsFeed() throws ServiceException, IOException {
        URL feedUrl = new URL(FEED_URI_BASE + "/" + getBlogID() + "/posts/default");
        Feed feed = getService().getFeed(feedUrl, Feed.class);
        return feed;
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="getPostByID()">
    /**
     * Получение поста по ID
     * @param postID ID поста
     * @return Пост
     * @throws ServiceException Ошибка сервиса...
     * @throws IOException IO ошибка
     */
    public Entry getPostByID(String postID) throws ServiceException, IOException {
        if (postID == null) {            
            throw new IllegalArgumentException("postID==null");
        }
        URL url = new URL(FEED_URI_BASE + "/" + getBlogID() + "/posts/default/" + postID);
        return getService().getEntry(url, Entry.class);
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="clearPasswords()">
    @Do(desc = "tipDoClearPasswords")
    public void clearPasswords() {
        removeStoredArg("gmailPassword");
        removeStoredArg("gmailLogin");
    }// </editor-fold>
    
    protected Convertor<DateTime,String> dateTimeConvertor = new Convertor<DateTime, String>() {
        @Override
        public String convert(DateTime from) {
            long shift = getTimeShift();
            Date date = new Date(from.getValue()-shift);
            return dateParser().format(date);
        }
    };
    
    // <editor-fold defaultstate="collapsed" desc="Обзор и экспорт постов">
    @Do(desc = "tipDoLookupBlogs")
    public void lookupBlogs() {
        try {
            URL feedUrl = new URL(METAFEED_URL);
            Feed feed = getService().getFeed(feedUrl, Feed.class);
            println(feed, blogIDConvertor);
        } catch (IOException ex) {
            throw new Error(ex.getMessage(), ex);
        } catch (ServiceException ex) {
            throw new Error(ex.getMessage(), ex);
        }
    }
    
    @Do(desc = "tipDoLookupPosts")
    public void lookupPosts() {
        try {
            println(getPostsFeed(), postIDConvertor);
        } catch (IOException ex) {
            throw new Error(ex.getMessage(), ex);
        } catch (ServiceException ex) {
            throw new Error(ex.getMessage(), ex);
        }
    }
    
    public void exportPost(Entry e) {
        if (e == null) {            
            throw new IllegalArgumentException("e==null");
        }
        
        String fileTmpl = getExportFileTmpl();
        if (fileTmpl == null)
            throw new ArgumentNotSetException("exportFileTmpl");
        
        Map<String, String> props = PostHelper.getFlatProperties(e, postIDConvertor,dateTimeConvertor);
        File f = new File(TextUtil.template(fileTmpl, props));
        
        File fdir = f.getParentFile();
        if( fdir!=null ){
            if( !fdir.exists() ){
                fdir.mkdirs();
            }
        }
        
        PostHelper.export(f, e, getHtmlTmpl(), getHtmlMetaTmpl(),dateTimeConvertor);
        
        System.out.println(messages().postExportedTo(props.get("title"), f));
    }
    
    @Do(desc = "tipDoExportPosts")
    public void exportPosts() {
        try {
            Feed f = getPostsFeed();
            int co = 0;
            int tot = f.getTotalResults();
            boolean totCoUndef = tot == Query.UNDEFINED;
            int max = totCoUndef ? getLimitEntries() : tot;
            for (Entry e : new EntryIterable(f)) {
                co++;
                if (co > max)
                    break;
                exportPost(e);
            }
        } catch (IOException ex) {
            throw new Error(ex.getMessage(), ex);
        } catch (ServiceException ex) {
            throw new Error(ex.getMessage(), ex);
        }
    }
    
    @Do(desc = "tipDoExportPost")
    public void exportPost() {
        try {
            String postId = getPostID();
            Entry e = getPostByID(postId);
            if (e != null) {
                exportPost(e);
            }
        } catch (IOException ex) {
            throw new Error(ex.getMessage(), ex);
        } catch (ServiceException ex) {
            throw new Error(ex.getMessage(), ex);
        }
    }// </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="dateFormat">
    private SimpleDateFormat dateParser = null;

    /**
     * Парсер даты
     * @return Парсер даты
     */
    private SimpleDateFormat dateParser() {
        if (dateParser != null)
            return dateParser;
        String ptrn = getDateFormat();
        if( ptrn==null )ptrn = "yyyy-MM-dd HH:mm";
        dateParser = new SimpleDateFormat(ptrn);
        return dateParser;
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Добавить пост, очиска html">
    @Do(desc = "tipDoAddPost")
    public void addPost() {
        try {
            String fileName = getAddPostFile();
            if (fileName == null) {                
                throw new ArgumentNotSetException("addPostFile");
            }
            File file = new File(fileName);
            HtmlFile htmlFile = HtmlFile.read(file);
            String htmlPostContent = htmlFile.getBody();
            if (htmlPostContent == null)
                throw new Error(messages().htmlBodyNotFound(file));
            
            String title = null;
            if (title == null)
                title = getPostTitle();
            if (title == null)
                title = htmlFile.getTitle();
            if (title == null)
                throw new ArgumentNotSetException("postTitle");
            
            boolean isDraftPost = isDraftPost();
            long postTimeShift = getTimeShift();
            Date postDate = new Date(htmlFile.getLastModifiedDate().getTime() + postTimeShift);
            
            Entry e = new Entry();
            e.setTitle(new PlainTextConstruct(title));
            e.setDraft(isDraftPost);
            e.setContent(new HtmlTextConstruct(htmlFile.getBody()));
            
            java.util.TimeZone tz = getTimeZone();
            if (tz == null)
                throw new ArgumentNotSetException("timeZone");
            
            Date pubDate = getPublishedDate()==null ? postDate : getPublishedDate();
            Date edtDate = getEditedDate()==null ? postDate : getEditedDate();
            Date updDate = getUpdatedDate()==null ? postDate : getUpdatedDate();
            
            e.setPublished(new DateTime(pubDate, tz));
            e.setEdited(new DateTime(edtDate, tz));
            e.setUpdated(new DateTime(updDate, tz));
            
            String blogId = getBlogID();
            if (blogId == null)
                throw new ArgumentNotSetException("blogID");
            
            BloggerService srvc = getService();
            URL url = new URL(FEED_URI_BASE + "/" + blogId + POSTS_FEED_URI_SUFFIX);
            srvc.insert(url, e);
        } catch (IOException ex) {
            throw new Error(ex.getMessage(), ex);
        } catch (ServiceException ex) {
            throw new Error(ex.getMessage(), ex);
        }
    }
    
    @Do(desc = "tipDoCleanPost")
    public void cleanPost() {
        String fileName = getAddPostFile();
        if (fileName == null) {            
            throw new ArgumentNotSetException("addPostFile");
        }
        File file = new File(fileName);
        HtmlFile htmlFile = HtmlFile.read(file);
        String htmlPostContent = htmlFile.getBody();
        if (htmlPostContent == null)
            throw new Error(messages().htmlBodyNotFound(file));
        
        String title = null;
        if (title == null)
            title = getPostTitle();
        if (title == null)
            title = htmlFile.getTitle();
        if (title == null)
            throw new ArgumentNotSetException("postTitle");
        
        String sExportFile = getExportFile();
        if (sExportFile == null)
            throw new ArgumentNotSetException("exportFile");
        File _exportFile = new File(sExportFile);
        
        Charset cs = FileUtil.UTF8();
        
        Map<String, String> t = new HashMap<String, String>();
        t.put("charset", cs.name());
        String metaCharset = "<meta http-equiv=\"content-type\" content=\"text/html; charset={charset}\"/>";
        metaCharset = TextUtil.template(metaCharset, t);
        
        t.put("title", TextUtil.htmlEncode(title));
        String headTitle = "<title>{title}</title>";
        headTitle = TextUtil.template(headTitle, t);
        
        t.put("metaCharset", metaCharset);
        t.put("title", headTitle);
        String head = "{metaCharset}\n{title}\n";
        head = TextUtil.template(head, t);
        
        t.put("head", head);
        t.put("body", htmlPostContent);
        String fileBody = "<html><head>{head}</head><body>{body}</body></html>";
        fileBody = TextUtil.template(fileBody, t);
        
        FileUtil.writeAllText(_exportFile, cs, fileBody);
    }// </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc="Входная точка приложения">
    /**
     * @param args Аргументы коммандной строки
     */
    public static void main(String[] args) {
        // Чтение параметров
        Map<String, String> m = new HashMap<String, String>();
        m.putAll(System.getenv());
        for (int i = 0; i < args.length - 1; i++) {
            if (args[i].startsWith("-") && args[i].length() > 0) {
                m.put(args[i].substring(1), args[i + 1]);
            }
        }
        
        // Анализ переданых параметров
        boolean printException = 
                m.containsKey("printException") ? 
                (
                    m.get("printException").equalsIgnoreCase("true") ||
                    m.get("printException").equalsIgnoreCase("on")
                ) 
                : true;

        boolean verbose = 
                m.containsKey("verbose") ? 
                (
                    m.get("verbose").equalsIgnoreCase("true") ||
                    m.get("verbose").equalsIgnoreCase("on")
                ) 
                : false;
        
        boolean logException = 
                m.containsKey("logException") ? 
                (
                    m.get("logException").equalsIgnoreCase("true") ||
                    m.get("logException").equalsIgnoreCase("on")
                ) 
                : false;

        boolean printExceptionStack = 
                m.containsKey("printExceptionStack") ? 
                (
                    m.get("printExceptionStack").equalsIgnoreCase("true") ||
                    m.get("printExceptionStack").equalsIgnoreCase("on")
                ) 
                : false;
        
        try {
            // Определяем расположение домашнего каталога программы
            File appd = getAppDir(m);

            // Инициализация сообщений
            initMessages(appd);
            
            String version = getVersion();
            if( version==null )version="?";
            
            String title = getTitle();
            if( title==null )title = "?";
            
            String vendor = getVendor();
            if( vendor==null )vendor = "?";
            
			if( verbose )
            System.out.println(messages().logo(title, vendor, version));
        
            CLI cli = new CLI(m,appd);
            printException = cli.argument("printException", printException, false);
            printExceptionStack = cli.argument("printExceptionStack", printException, false);
            logException = cli.argument("logException", logException, false);
            
            cli.eval();
        } catch (Throwable ex) {
            if( printException )print(ex,0,null,printExceptionStack);
            if( logException )Logger.getLogger(CLI.class.getName()).log(Level.SEVERE, null, ex);
            System.exit(1);
        }
    }
    
    private static String getVersion(){
        return CLI.class.getPackage().getImplementationVersion();
    }
    private static String getTitle(){
        return CLI.class.getPackage().getImplementationTitle();
    }
    private static String getVendor(){
        return CLI.class.getPackage().getImplementationVendor();
    }
    
    private static File getAppDir(Map<String, String> m){
        File appd = m.containsKey("appDir") ? new File(m.get("appDir")) : null;
        if( appd==null ){
            String def = System.getProperty("user.home", null);
            if (def != null) {
                String fileSeparator = System.getProperty("file.separator", "/");
                def = TextUtil.trimEnd(def, fileSeparator) + "/.tv/cofe/blogger".replace("/", fileSeparator);
            }else{
                throw new Error("System.getProperty(\"user.home\") == null");
            }
            appd = new File(def);
        }
        return appd;
    }
    
    private static void print(Throwable ex,int level,IndentStackWriter writer,boolean printExceptionStack){
        if( level<0 )level = 0;
        if( writer==null )writer = new IndentStackWriter(new OutputStreamWriter(System.out));
        
        String message = ex.getMessage();
        String clazz = ex.getClass().getName();
        
        writer.level(level);
        writer.indent("  ");
        
        writer.print(clazz);
        if( message!=null ){
            writer.print(": ");
            writer.print(message);
        }
        writer.println();
        writer.flush();
        
        Throwable cex = ex.getCause();
        if( cex!=null ){
            print( cex, level+1, writer, printExceptionStack );
        }
        
        if( cex==null && printExceptionStack ){
            writer.level(level);
            writer.println();
            writer.println("Stack:");
            ex.printStackTrace(writer);
            writer.flush();
        }
    }
    // </editor-fold>
}